/* SPU2-X, A plugin for Emulating the Sound Processing Unit of the Playstation 2
 * Developed and maintained by the Pcsx2 Development Team.
 *
 * Original portions from SPU2ghz are (c) 2008 by David Quintana [gigaherz]
 *
 * SPU2-X is free software: you can redistribute it and/or modify it under the terms
 * of the GNU Lesser General Public License as published by the Free Software Found-
 * ation, either version 3 of the License, or (at your option) any later version.
 *
 * SPU2-X is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE.  See the GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with SPU2-X.  If not, see <http://www.gnu.org/licenses/>.
 */

#include "Global.h"
#include "PS2E-spu2.h"
#include "Dma.h"
#include "Dialogs.h"

#ifdef __APPLE__
#include "PS2Eext.h"
#endif

#include "svnrev.h"

#ifdef _MSC_VER
#define snprintf sprintf_s
#endif
// PCSX2 expects ASNI, not unicode, so this MUST always be char...
static char libraryName[256];


static bool IsOpened = false;
static bool IsInitialized = false;

static u32 pClocks = 0;

u32 *cyclePtr = NULL;
u32 lClocks = 0;

#ifdef _MSC_VER
HINSTANCE hInstance;
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
    if (dwReason == DLL_PROCESS_ATTACH) {
        hInstance = hinstDLL;
    } else if (dwReason == DLL_PROCESS_DETACH) {
        // TODO : perform shutdown procedure, just in case PCSX2 itself failed
        // to for some reason..
    }
    return TRUE;
}
#endif

static void InitLibraryName()
{
#ifdef SPU2X_PUBLIC_RELEASE

    // Public Release!
    // Output a simplified string that's just our name:

    strcpy(libraryName, "SPU2-X");

#else
#ifdef SVN_REV_UNKNOWN

    // Unknown revision.
    // Output a name that includes devbuild status but not
    // subversion revision tags:

    strcpy(libraryName, "SPU2-X"
#ifdef DEBUG_FAST
                        "-Debug"
#elif defined(PCSX2_DEBUG)
                        "-Debug/Strict" // strict debugging is slow!
#elif defined(PCSX2_DEVBUILD)
                        "-Dev"
#else
                        ""
#endif
           );

#else

    // Use TortoiseSVN's SubWCRev utility's output
    // to label the specific revision:

    snprintf(libraryName, 255, "SPU2-X %lld%s"
#ifdef DEBUG_FAST
                               "-Debug"
#elif defined(PCSX2_DEBUG)
                               "-Debug/Strict" // strict debugging is slow!
#elif defined(PCSX2_DEVBUILD)
                               "-Dev"
#else
                               ""
#endif
             ,
             SVN_REV, SVN_MODS ? "m" : "");
#endif
#endif
}

//static bool cpu_detected = false;

static bool CheckSSE()
{
    return true;

#if 0
	if( !cpu_detected )
	{
		cpudetectInit();
		cpu_detected = true;
	}
	if( !x86caps.hasStreamingSIMDExtensions || !x86caps.hasStreamingSIMD2Extensions )
	{
		SysMessage( "Your CPU does not support SSE2 instructions.\nThe SPU2-X plugin requires SSE2 to run." );
		return false;
	}
	return true;
#endif
}

EXPORT_C_(u32)
PS2EgetLibType()
{
    return PS2E_LT_SPU2;
}

EXPORT_C_(char *)
PS2EgetLibName()
{
    InitLibraryName();
    return libraryName;
}

EXPORT_C_(u32)
PS2EgetLibVersion2(u32 type)
{
    return (PS2E_SPU2_VERSION << 16) | (VersionInfo::Release << 8) | VersionInfo::Revision;
}

EXPORT_C_(void)
SPU2configure()
{
    if (!CheckSSE())
        return;
    configure();
}

EXPORT_C_(s32)
SPU2test()
{
    if (!CheckSSE())
        return -1;

#ifdef _WIN32
    if (IsWindows8OrGreater()) {
        for (int n = 0; mods[n] != nullptr; ++n) {
            if (mods[n] == XAudio2_27_Out) {
                mods[n] = XAudio2Out;
                break;
            }
        }
    }
#endif

    ReadSettings();
    if (SndBuffer::Test() != 0) {
        // TODO : Implement a proper dialog that allows the user to test different audio out drivers.
        const wchar_t *wtf = mods[OutputModule]->GetIdent();
        SysMessage(L"The '%s' driver test failed.  Please configure\na different SoundOut module and try again.", wtf);
        return -1;
    }

    return 0;
}

// --------------------------------------------------------------------------------------
//  DMA 4/7 Callbacks from Core Emulator
// --------------------------------------------------------------------------------------

u16 *DMABaseAddr;
void (*_irqcallback)();
void (*dma4callback)();
void (*dma7callback)();

EXPORT_C_(u32)
CALLBACK SPU2ReadMemAddr(int core)
{
    return Cores[core].MADR;
}
EXPORT_C_(void)
CALLBACK SPU2WriteMemAddr(int core, u32 value)
{
    Cores[core].MADR = value;
}

EXPORT_C_(void)
CALLBACK SPU2setDMABaseAddr(uptr baseaddr)
{
    DMABaseAddr = (u16 *)baseaddr;
}

EXPORT_C_(void)
CALLBACK SPU2setSettingsDir(const char *dir)
{
    CfgSetSettingsDir(dir);
}

EXPORT_C_(void)
CALLBACK SPU2setLogDir(const char *dir)
{
    CfgSetLogDir(dir);
}

EXPORT_C_(s32)
SPU2dmaRead(s32 channel, u32 *data, u32 bytesLeft, u32 *bytesProcessed)
{
    if (channel == 4)
        return Cores[0].NewDmaRead(data, bytesLeft, bytesProcessed);
    else
        return Cores[1].NewDmaRead(data, bytesLeft, bytesProcessed);
}

EXPORT_C_(s32)
SPU2dmaWrite(s32 channel, u32 *data, u32 bytesLeft, u32 *bytesProcessed)
{
    if (channel == 4)
        return Cores[0].NewDmaWrite(data, bytesLeft, bytesProcessed);
    else
        return Cores[1].NewDmaWrite(data, bytesLeft, bytesProcessed);
}

EXPORT_C_(void)
SPU2dmaInterrupt(s32 channel)
{
    if (channel == 4)
        return Cores[0].NewDmaInterrupt();
    else
        return Cores[1].NewDmaInterrupt();
}

#ifdef ENABLE_NEW_IOPDMA_SPU2
EXPORT_C_(void)
SPU2irqCallback(void (*SPU2callback)())
{
    _irqcallback = SPU2callback;
}
#else
EXPORT_C_(void)
SPU2irqCallback(void (*SPU2callback)(), void (*DMA4callback)(), void (*DMA7callback)())
{
    _irqcallback = SPU2callback;
    dma4callback = DMA4callback;
    dma7callback = DMA7callback;
}
#endif

EXPORT_C_(void)
CALLBACK SPU2readDMA4Mem(u16 *pMem, u32 size) // size now in 16bit units
{
    if (cyclePtr != NULL)
        TimeUpdate(*cyclePtr);

    FileLog("[%10d] SPU2 readDMA4Mem size %x\n", Cycles, size << 1);
    Cores[0].DoDMAread(pMem, size);
}

EXPORT_C_(void)
CALLBACK SPU2writeDMA4Mem(u16 *pMem, u32 size) // size now in 16bit units
{
    if (cyclePtr != NULL)
        TimeUpdate(*cyclePtr);

    FileLog("[%10d] SPU2 writeDMA4Mem size %x at address %x\n", Cycles, size << 1, Cores[0].TSA);
#ifdef S2R_ENABLE
    if (!replay_mode)
        s2r_writedma4(Cycles, pMem, size);
#endif
    Cores[0].DoDMAwrite(pMem, size);
}

EXPORT_C_(void)
CALLBACK SPU2interruptDMA4()
{
    FileLog("[%10d] SPU2 interruptDMA4\n", Cycles);
    Cores[0].Regs.STATX |= 0x80;
    //Cores[0].Regs.ATTR &= ~0x30;
}

EXPORT_C_(void)
CALLBACK SPU2interruptDMA7()
{
    FileLog("[%10d] SPU2 interruptDMA7\n", Cycles);
    Cores[1].Regs.STATX |= 0x80;
    //Cores[1].Regs.ATTR &= ~0x30;
}

EXPORT_C_(void)
CALLBACK SPU2readDMA7Mem(u16 *pMem, u32 size)
{
    if (cyclePtr != NULL)
        TimeUpdate(*cyclePtr);

    FileLog("[%10d] SPU2 readDMA7Mem size %x\n", Cycles, size << 1);
    Cores[1].DoDMAread(pMem, size);
}

EXPORT_C_(void)
CALLBACK SPU2writeDMA7Mem(u16 *pMem, u32 size)
{
    if (cyclePtr != NULL)
        TimeUpdate(*cyclePtr);

    FileLog("[%10d] SPU2 writeDMA7Mem size %x at address %x\n", Cycles, size << 1, Cores[1].TSA);
#ifdef S2R_ENABLE
    if (!replay_mode)
        s2r_writedma7(Cycles, pMem, size);
#endif
    Cores[1].DoDMAwrite(pMem, size);
}

EXPORT_C_(void)
SPU2reset()
{
    memset(spu2regs, 0, 0x010000);
    memset(_spu2mem, 0, 0x200000);
    memset(_spu2mem + 0x2800, 7, 0x10); // from BIOS reversal. Locks the voices so they don't run free.
    Cores[0].Init(0);
    Cores[1].Init(1);
}

EXPORT_C_(s32)
SPU2init()
{
    assert(regtable[0x400] == NULL);

    if (IsInitialized) {
        printf(" * SPU2-X: Already initialized - Ignoring SPU2init signal.");
        return 0;
    }

    IsInitialized = true;

    ReadSettings();

#ifdef SPU2_LOG
    if (AccessLog()) {
        spu2Log = OpenLog(AccessLogFileName);
        setvbuf(spu2Log, NULL, _IONBF, 0);
        FileLog("SPU2init\n");
    }
#endif
    srand((unsigned)time(NULL));

    spu2regs = (s16 *)malloc(0x010000);
    _spu2mem = (s16 *)malloc(0x200000);

    // adpcm decoder cache:
    //  the cache data size is determined by taking the number of adpcm blocks
    //  (2MB / 16) and multiplying it by the decoded block size (28 samples).
    //  Thus: pcm_cache_data = 7,340,032 bytes (ouch!)
    //  Expanded: 16 bytes expands to 56 bytes [3.5:1 ratio]
    //    Resulting in 2MB * 3.5.

    pcm_cache_data = (PcmCacheEntry *)calloc(pcm_BlockCount, sizeof(PcmCacheEntry));

    if ((spu2regs == NULL) || (_spu2mem == NULL) || (pcm_cache_data == NULL)) {
        SysMessage("SPU2-X: Error allocating Memory\n");
        return -1;
    }

    // Patch up a copy of regtable that directly maps "NULLs" to SPU2 memory.

    memcpy(regtable, regtable_original, sizeof(regtable));

    for (uint mem = 0; mem < 0x800; mem++) {
        u16 *ptr = regtable[mem >> 1];
        if (!ptr) {
            regtable[mem >> 1] = &(spu2Ru16(mem));
        }
    }

    SPU2reset();

    DMALogOpen();
    InitADSR();

#ifdef S2R_ENABLE
    if (!replay_mode)
        s2r_open(Cycles, "replay_dump.s2r");
#endif
    return 0;
}

#ifdef _MSC_VER
// Bit ugly to have this here instead of in RealttimeDebugger.cpp, but meh :p
extern bool debugDialogOpen;
extern HWND hDebugDialog;

static INT_PTR CALLBACK DebugProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    int wmId;

    switch (uMsg) {
        case WM_PAINT:
            return FALSE;
        case WM_INITDIALOG: {
            debugDialogOpen = true;
        } break;

        case WM_COMMAND:
            wmId = LOWORD(wParam);
            // Parse the menu selections:
            switch (wmId) {
                case IDOK:
                case IDCANCEL:
                    debugDialogOpen = false;
                    EndDialog(hWnd, 0);
                    break;
                default:
                    return FALSE;
            }
            break;

        default:
            return FALSE;
    }
    return TRUE;
}
#endif
uptr gsWindowHandle = 0;

EXPORT_C_(s32)
SPU2open(void *pDsp)
{
    if (IsOpened)
        return 0;

    FileLog("[%10d] SPU2 Open\n", Cycles);

    if (pDsp != NULL)
        gsWindowHandle = *(uptr *)pDsp;
    else
        gsWindowHandle = 0;

#ifdef _MSC_VER
#ifdef PCSX2_DEVBUILD // Define may not be needed but not tested yet. Better make sure.
    if (IsDevBuild && VisualDebug()) {
        if (debugDialogOpen == 0) {
            hDebugDialog = CreateDialogParam(hInstance, MAKEINTRESOURCE(IDD_DEBUG), 0, DebugProc, 0);
            ShowWindow(hDebugDialog, SW_SHOWNORMAL);
            debugDialogOpen = 1;
        }
    } else if (debugDialogOpen) {
        DestroyWindow(hDebugDialog);
        debugDialogOpen = 0;
    }
#endif
#endif

    IsOpened = true;
    lClocks = (cyclePtr != NULL) ? *cyclePtr : 0;

    try {
        SndBuffer::Init();

#ifndef __POSIX__
        DspLoadLibrary(dspPlugin, dspPluginModule);
#endif
        WaveDump::Open();
    } catch (std::exception &ex) {
        fprintf(stderr, "SPU2-X Error: Could not initialize device, or something.\nReason: %s", ex.what());
        SPU2close();
        return -1;
    }
    return 0;
}

EXPORT_C_(void)
SPU2close()
{
    if (!IsOpened)
        return;
    IsOpened = false;

    FileLog("[%10d] SPU2 Close\n", Cycles);

#ifndef __POSIX__
    DspCloseLibrary();
#endif

    SndBuffer::Cleanup();
}

EXPORT_C_(void)
SPU2shutdown()
{
    if (!IsInitialized)
        return;
    IsInitialized = false;

    ConLog("* SPU2-X: Shutting down.\n");

    SPU2close();

#ifdef S2R_ENABLE
    if (!replay_mode)
        s2r_close();
#endif

    DoFullDump();
#ifdef STREAM_DUMP
    fclose(il0);
    fclose(il1);
#endif
#ifdef EFFECTS_DUMP
    fclose(el0);
    fclose(el1);
#endif
    WaveDump::Close();

    DMALogClose();

    safe_free(spu2regs);
    safe_free(_spu2mem);
    safe_free(pcm_cache_data);


#ifdef SPU2_LOG
    if (!AccessLog())
        return;
    FileLog("[%10d] SPU2shutdown\n", Cycles);
    if (spu2Log)
        fclose(spu2Log);
#endif
}

EXPORT_C_(void)
SPU2setClockPtr(u32 *ptr)
{
    cyclePtr = ptr;
}

#ifdef DEBUG_KEYS
static u32 lastTicks;
static bool lState[6];
#endif

EXPORT_C_(void)
SPU2async(u32 cycles)
{
    DspUpdate();

    if (cyclePtr != NULL) {
        TimeUpdate(*cyclePtr);
    } else {
        pClocks += cycles;
        TimeUpdate(pClocks);
    }

#ifdef DEBUG_KEYS
    u32 curTicks = GetTickCount();
    if ((curTicks - lastTicks) >= 50) {
        int oldI = Interpolation;
        bool cState[6];
        for (int i = 0; i < 6; i++) {
            cState[i] = !!(GetAsyncKeyState(VK_NUMPAD0 + i) & 0x8000);

            if ((cState[i] && !lState[i]) && i != 5)
                Interpolation = i;

            if ((cState[i] && !lState[i]) && i == 5) {
                postprocess_filter_enabled = !postprocess_filter_enabled;
                printf("Post process filters %s \n", postprocess_filter_enabled ? "enabled" : "disabled");
            }

            lState[i] = cState[i];
        }

        if (Interpolation != oldI) {
            printf("Interpolation set to %d", Interpolation);
            switch (Interpolation) {
                case 0:
                    printf(" - Nearest.\n");
                    break;
                case 1:
                    printf(" - Linear.\n");
                    break;
                case 2:
                    printf(" - Cubic.\n");
                    break;
                case 3:
                    printf(" - Hermite.\n");
                    break;
                case 4:
                    printf(" - Catmull-Rom.\n");
                    break;
                default:
                    printf(" (unknown).\n");
                    break;
            }
        }

        lastTicks = curTicks;
    }
#endif
}

EXPORT_C_(u16)
SPU2read(u32 rmem)
{
    //	if(!replay_mode)
    //		s2r_readreg(Cycles,rmem);

    u16 ret = 0xDEAD;
    u32 core = 0, mem = rmem & 0xFFFF, omem = mem;
    if (mem & 0x400) {
        omem ^= 0x400;
        core = 1;
    }

    if (omem == 0x1f9001AC) {
        ret = Cores[core].DmaRead();
    } else {
        if (cyclePtr != NULL)
            TimeUpdate(*cyclePtr);

        if (rmem >> 16 == 0x1f80) {
            ret = Cores[0].ReadRegPS1(rmem);
        } else if (mem >= 0x800) {
            ret = spu2Ru16(mem);
            ConLog("* SPU2-X: Read from reg>=0x800: %x value %x\n", mem, ret);
        } else {
            ret = *(regtable[(mem >> 1)]);
            //FileLog("[%10d] SPU2 read mem %x (core %d, register %x): %x\n",Cycles, mem, core, (omem & 0x7ff), ret);
            SPU2writeLog("read", rmem, ret);
        }
    }

    return ret;
}

EXPORT_C_(void)
SPU2write(u32 rmem, u16 value)
{
#ifdef S2R_ENABLE
    if (!replay_mode)
        s2r_writereg(Cycles, rmem, value);
#endif

    // Note: Reverb/Effects are very sensitive to having precise update timings.
    // If the SPU2 isn't in in sync with the IOP, samples can end up playing at rather
    // incorrect pitches and loop lengths.

    if (cyclePtr != NULL)
        TimeUpdate(*cyclePtr);

    if (rmem >> 16 == 0x1f80)
        Cores[0].WriteRegPS1(rmem, value);
    else {
        SPU2writeLog("write", rmem, value);
        SPU2_FastWrite(rmem, value);
    }
}

// if start is 1, starts recording spu2 data, else stops
// returns a non zero value if successful
// for now, pData is not used
EXPORT_C_(int)
SPU2setupRecording(int start, void *pData)
{
    if (start == 0)
        RecordStop();
    else if (start == 1)
        RecordStart();

    return 0;
}

EXPORT_C_(s32)
SPU2freeze(int mode, freezeData *data)
{
    pxAssume(data != NULL);
    if (!data) {
        printf("SPU2-X savestate null pointer!\n");
        return -1;
    }

    if (mode == FREEZE_SIZE) {
        data->size = Savestate::SizeIt();
        return 0;
    }

    pxAssume(mode == FREEZE_LOAD || mode == FREEZE_SAVE);

    if (data->data == NULL) {
        printf("SPU2-X savestate null pointer!\n");
        return -1;
    }

    Savestate::DataBlock &spud = (Savestate::DataBlock &)*(data->data);

    switch (mode) {
        case FREEZE_LOAD:
            return Savestate::ThawIt(spud);
        case FREEZE_SAVE:
            return Savestate::FreezeIt(spud);

            jNO_DEFAULT;
    }

    // technically unreachable, but kills a warning:
    return 0;
}
